---
title: "The pretty Klein j-invariant function"
author: "Stéphane Laurent"
date: '2023-09-14'
tags: R, graphics, maths
rbloggers: yes
output:
md_document:
variant: markdown
preserve_yaml: true
html_document:
highlight: kate
keep_md: no
highlighter: pandoc-solarized
---
Here are four representations of the [Klein j-invariant
function](https://en.wikipedia.org/wiki/J-invariant):
![](./figures/KleinFibo_5-6-7-9.png)
The Klein j-invariant function is a complex function defined on the
upper half-plane of the complex numbers. On the above pictures, we
mapped it to a circle with the inverse modified Cayley transformation,
which is defined by $$
\Psi(z) = i + 2iz / (i - z)
$$ and which maps the unit circle to the upper half-plane. Then I use
some [color maps](https://en.wikipedia.org/wiki/Domain_coloring) to do
these pictures. These color maps are available in the
[**RcppColors**](https://github.com/stla/RcppColors) package.
I particularly like the first picture and I did an animation of it. The
purpose of this blog post is to explain how I did.
The animation is made with the help of a modular Möbius transformation
$$
R(z) = - \frac{1}{z+1}
$$ and its generalized powers $$
R^t(z) =
\frac{\Bigl(\sqrt{3}\cos\bigl(\frac{\pi t}{3}\bigr)
- \sin\bigl(\frac{\pi t}{3}\bigr)\Bigr) z - 2\sin\bigl(\frac{\pi t}{3}\bigr)}
{2\sin\bigl(\frac{\pi t}{3}\bigr) z + \sqrt{3}\cos\bigl(\frac{\pi t}{3}\bigr)
+ \sin\bigl(\frac{\pi t}{3}\bigr)}
$$ This transformation is of order three, i.e. $R^3(z)=z$. I found it in
[this
paper](https://www3.risc.jku.at/publications/download/risc_5011/DiplomaThesisPonweiser.pdf).
Let's define it in R:
``` r
R <- function(z, t) {
a <- pi*t/3
((sqrt(3)*cos(a) - sin(a)) * z - 2*sin(a))/
(2*sin(a) * z + sqrt(3)*cos(a) + sin(a))
}
```
as well as the inverse modified Cayley transformation (also found in the
above paper):
``` r
Psi <- function(z) {
1i + (2i*z) / (1i - z)
}
```
This function $\Psi$ is the one we will use to represent the Klein
j-invariant function on a circle.
Now, I made a grid of the unit circle. Well, not really. The Klein
j-invariant function is available in the **jacobi** package under the
name `kleinj`. But when a complex number `tau` in the upper half-plane
(i.e. with positive imaginary part) is too close to the real line, the
`kleinj` function fails. The inverse modified Cayley transform $\Psi$
maps the boundary of the unit circle to the real line. So I take the
centered circle of radius $0.96$ instead of the unit circle. I apply
$\Psi$ to each point $z$ in this circle, and I return $\Psi(z)$ or
$-1/\Psi(z)$. Later I will apply $j$ (the Klein j-invariant function) to
the results. The reason for which I return $-1/\Psi(z)$ sometimes (when
$\Im(z)<0$) is that $j(\tau) = j(-1/\tau)$ and I found that applying
this transformation avoids some failures of `kleinj`.
``` r
library(jacobi)
f <- function(x, y) {
z <- complex(real = x, imaginary = y)
w <- Psi(z)
ifelse(
Mod(z) > 0.96,
NA_complex_,
ifelse(
y < 0, -1/w, w
)
)
}
x <- seq(-1, 1, length.out = 2048L)
y <- seq(-1, 1, length.out = 2048L)
Z <- outer(x, y, f)
K <- kleinj(Z) / 1728
```
In fact, there are two Klein j-invariant functions, differing by a
factor of $1728$. That's why I divide by $1728$: I take the other Klein
function.
We're almost ready. But there's an issue and we will have to overcome
it. Let's load the **RcppColors** package, and we will use the
`colorMap5` color map. Observe the "first" picture, the one obtained
without applying $R^t$:
``` r
image <- colorMap5(K), bkgcolor = "white")
svg("x.svg")
opar <- par(mar = c(0,0,0,0))
plot(
c(-1, 1), c(-1, 1), type = "n", xaxs = "i", yaxs = "i",
xlab = NA, ylab = NA, axes = FALSE, asp = 1
)
rasterImage(image, -1, -1, 1, 1)
par(opar)
dev.off()
rsvg::rsvg_png(
"x.svg", "Klein_t0.png", width = 512, height = 512
)
```
![](./figures/Klein_t0.png)
And now, observe the picture obtained by applying $R^{0.01}$;
``` r
image <- colorMap5(R(K, 0.01), bkgcolor = "white")
svg("x.svg")
opar <- par(mar = c(0,0,0,0))
plot(
c(-1, 1), c(-1, 1), type = "n", xaxs = "i", yaxs = "i",
xlab = NA, ylab = NA, axes = FALSE, asp = 1
)
rasterImage(image, -1, -1, 1, 1)
par(opar)
dev.off()
rsvg::rsvg_png(
"x.svg", "Klein_t001.png", width = 512, height = 512
)
```
![](./figures/Klein_t001.png)
There's a "jump": the transition from the first picture to the second
one is too fast. So, since we want to make the pictures with $t$ running
from $0$ to $3$, we need to slow down the transition when there's such a
jump. We will use a "smooth staircase function" for that. A nice one is
$x - \sin(x)$:
``` r
xmsinx <- function(x) x - sin(x)
curve(xmsinx, from = -2*pi, to = 2*pi, lwd = 2)
```
``{=html}
I firstly tried to use this function but this is not enough: the
transitions are still too fast. We can iterate this function to get more
slowness:
``` r
curve(xmsinx(xmsinx(x)), from = -2*pi, to = 2*pi, lwd = 2)
```
``{=html}
This one is good. We need to modify it in order that the "stairs" fit
the "jumps". We will range $t$ from $-2\pi$ to $\pi$ and we will apply
the modified smooth stair case function $s$ defined by
``` r
s <- function(x) {
(xmsinx(xmsinx(2*x)) + 4*pi) / (2*pi)
}
```
Applied to $t$ will give a range from $0$ to $3$.
We're ready! Let's make the animation frames:
``` r
t_ <- seq(-2*pi, pi, length.out= 91L)[-1L]
for(i in seq_along(t_)) {
KRt <- R(K, s(t_[i]))
image <- colorMap5(KRt, bkgcolor = bkgcol)
svg("x.svg")
opar <- par(mar = c(0,0,0,0))
plot(
c(-1, 1), c(-1, 1), type = "n", xaxs = "i", yaxs = "i",
xlab = NA, ylab = NA, axes = FALSE, asp = 1
)
rasterImage(image, -1, -1, 1, 1)
par(opar)
dev.off()
rsvg::rsvg_png(
"x.svg", sprintf("frame%03d.png", i), width = 512, height = 512
)
}
```
The frames are done. It remains to mount them to a GIF with **gifski**.
``` r
library(gifski)
pngFrames <- Sys.glob("frame*.png")
gifski(
pngFrames,
"Kleinj.gif",
width = 512, height = 512,
delay = 1/10
)
file.remove(pngFrames)
```
![](./figures/Kleinj.gif)